{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "5867e8c1",
   "metadata": {},
   "source": [
    "# 대화 기록 요약을 추가하는 방법\n",
    "\n",
    "![](image/langgraph-10.jpeg)\n",
    "\n",
    "대화 기록을 유지하는 것은 **지속성**의 가장 일반적인 사용 사례 중 하나입니다. 이는 대화를 지속하기 쉽게 만들어주는 장점이 있습니다. \n",
    "\n",
    "하지만 대화가 길어질수록 대화 기록이 누적되어 `context window`를 더 많이 차지하게 됩니다. 이는 `LLM` 호출이 더 비싸고 길어지며, 잠재적으로 오류가 발생할 수 있어 바람직하지 않을 수 있습니다. 이를 해결하기 위한 한 가지 방법은 현재까지의 대화 요약본을 생성하고, 이를 최근 `N` 개의 메시지와 함께 사용하는 것입니다. \n",
    "\n",
    "이 가이드에서는 이를 구현하는 방법의 예시를 살펴보겠습니다.\n",
    "\n",
    "다음과 같은 단계가 필요합니다.\n",
    "\n",
    "- 대화가 너무 긴지 확인 (메시지 수나 메시지 길이로 확인 가능)\n",
    "- 너무 길다면 요약본 생성 (이를 위한 프롬프트 필요)\n",
    "- 마지막 `N` 개의 메시지를 제외한 나머지 삭제\n",
    "\n",
    "이 과정에서 중요한 부분은 오래된 메시지를 삭제(`DeleteMessage`) 하는 것입니다. \n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "01ef813d",
   "metadata": {},
   "source": [
    "## 환경 설정"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "907729a0",
   "metadata": {},
   "outputs": [],
   "source": [
    "# API 키를 환경변수로 관리하기 위한 설정 파일\n",
    "from dotenv import load_dotenv\n",
    "\n",
    "# API 키 정보 로드\n",
    "load_dotenv()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23d58797",
   "metadata": {},
   "outputs": [],
   "source": [
    "# LangSmith 추적을 설정합니다. https://smith.langchain.com\n",
    "# !pip install -qU langchain-teddynote\n",
    "from langchain_teddynote import logging\n",
    "\n",
    "# 프로젝트 이름을 입력합니다.\n",
    "logging.langsmith(\"CH17-LangGraph-Modules\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c2bc956e",
   "metadata": {},
   "source": [
    "## 긴 대화를 요약하여 대화로 저장\n",
    "\n",
    "긴 대화에 대하여 요약본을 생성한 뒤, 기존의 대화를 삭제하고 요약본을 대화로 저장합니다.\n",
    "\n",
    "**조건**\n",
    "\n",
    "- 대화의 길이가 6개 초과일 경우 요약본을 생성"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "0100b1de",
   "metadata": {},
   "outputs": [],
   "source": [
    "from typing import Literal, Annotated\n",
    "from langchain_openai import ChatOpenAI\n",
    "from langchain_core.messages import SystemMessage, RemoveMessage, HumanMessage\n",
    "from langgraph.checkpoint.memory import MemorySaver\n",
    "from langgraph.graph import MessagesState, StateGraph, START\n",
    "from langgraph.graph.message import add_messages\n",
    "\n",
    "# 메모리 저장소 설정\n",
    "memory = MemorySaver()\n",
    "\n",
    "\n",
    "# 메시지 상태와 요약 정보를 포함하는 상태 클래스\n",
    "class State(MessagesState):\n",
    "    messages: Annotated[list, add_messages]\n",
    "    summary: str\n",
    "\n",
    "\n",
    "# 대화 및 요약을 위한 모델 초기화\n",
    "model = ChatOpenAI(model_name=\"gpt-4o-mini\", temperature=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "21455fbe",
   "metadata": {},
   "source": [
    "`ask_llm` 노드는 `messages` 를 llm 에 주입하여 답변을 얻습니다.\n",
    "\n",
    "만약, **이전의 대화 요약본**이 존재한다면, 이를 시스템 메시지로 추가하여 대화에 포함시킵니다.\n",
    "\n",
    "하지만, 이전의 대화 요약본이 존재하지 않는다면, 이전의 대화 내용만 사용합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "8c82b34d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def ask_llm(state: State):\n",
    "    # 이전 요약 정보 확인\n",
    "    summary = state.get(\"summary\", \"\")\n",
    "\n",
    "    # 이전 요약 정보가 있다면 시스템 메시지로 추가\n",
    "    if summary:\n",
    "        # 시스템 메시지 생성\n",
    "        system_message = f\"Summary of conversation earlier: {summary}\"\n",
    "        # 시스템 메시지와 이전 메시지 결합\n",
    "        messages = [SystemMessage(content=system_message)] + state[\"messages\"]\n",
    "    else:\n",
    "        # 이전 메시지만 사용\n",
    "        messages = state[\"messages\"]\n",
    "\n",
    "    # 모델 호출\n",
    "    response = model.invoke(messages)\n",
    "\n",
    "    # 응답 반환\n",
    "    return {\"messages\": [response]}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "640a4939",
   "metadata": {},
   "source": [
    "`should_continue` 노드는 대화의 길이가 6개 초과일 경우 요약 노드로 이동합니다.\n",
    "\n",
    "그렇지 않다면, 즉각 답변을 반환합니다. (`END` 노드로 이동)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "0b675f55",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langgraph.graph import END\n",
    "\n",
    "\n",
    "# 대화 종료 또는 요약 결정 로직\n",
    "def should_continue(state: State) -> Literal[\"summarize_conversation\", END]:\n",
    "    # 메시지 목록 확인\n",
    "    messages = state[\"messages\"]\n",
    "\n",
    "    # 메시지 수가 6개 초과라면 요약 노드로 이동\n",
    "    if len(messages) > 6:\n",
    "        return \"summarize_conversation\"\n",
    "    return END"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4db4d857",
   "metadata": {},
   "source": [
    "`summarize_conversation` 노드는 대화 내용을 요약하고, 오래된 메시지를 삭제합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "906c2481",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 대화 내용 요약 및 메시지 정리 로직\n",
    "def summarize_conversation(state: State):\n",
    "    # 이전 요약 정보 확인\n",
    "    summary = state.get(\"summary\", \"\")\n",
    "\n",
    "    # 이전 요약 정보가 있다면 요약 메시지 생성\n",
    "    if summary:\n",
    "        summary_message = (\n",
    "            f\"This is summary of the conversation to date: {summary}\\n\\n\"\n",
    "            \"Extend the summary by taking into account the new messages above in Korean:\"\n",
    "        )\n",
    "    else:\n",
    "        # 요약 메시지 생성\n",
    "        summary_message = \"Create a summary of the conversation above in Korean:\"\n",
    "\n",
    "    # 요약 메시지와 이전 메시지 결합\n",
    "    messages = state[\"messages\"] + [HumanMessage(content=summary_message)]\n",
    "    # 모델 호출\n",
    "    response = model.invoke(messages)\n",
    "    # 오래된 메시지 삭제\n",
    "    delete_messages = [RemoveMessage(id=m.id) for m in state[\"messages\"][:-2]]\n",
    "    # 요약 정보 반환\n",
    "    return {\"summary\": response.content, \"messages\": delete_messages}"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "8a654e69",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 워크플로우 그래프 초기화\n",
    "workflow = StateGraph(State)\n",
    "\n",
    "# 대화 및 요약 노드 추가\n",
    "workflow.add_node(\"conversation\", ask_llm)\n",
    "workflow.add_node(summarize_conversation)\n",
    "\n",
    "# 시작점을 대화 노드로 설정\n",
    "workflow.add_edge(START, \"conversation\")\n",
    "\n",
    "# 조건부 엣지 추가\n",
    "workflow.add_conditional_edges(\n",
    "    \"conversation\",\n",
    "    should_continue,\n",
    ")\n",
    "\n",
    "# 요약 노드에서 종료 노드로의 엣지 추가\n",
    "workflow.add_edge(\"summarize_conversation\", END)\n",
    "\n",
    "# 워크플로우 컴파일 및 메모리 체크포인터 설정\n",
    "app = workflow.compile(checkpointer=memory)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "33e9c6df",
   "metadata": {},
   "source": [
    "그래프를 시각화합니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e940c619",
   "metadata": {},
   "outputs": [],
   "source": [
    "from langchain_teddynote.graphs import visualize_graph\n",
    "\n",
    "visualize_graph(app)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3b813fd4",
   "metadata": {},
   "source": [
    "## 그래프 실행"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "a67abe86",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 업데이트 정보 출력 함수\n",
    "def print_update(update):\n",
    "    # 업데이트 딕셔너리 순회\n",
    "    for k, v in update.items():\n",
    "        # 메시지 목록 출력\n",
    "        for m in v[\"messages\"]:\n",
    "            m.pretty_print()\n",
    "        # 요약 정보 존재 시 출력\n",
    "        if \"summary\" in v:\n",
    "            print(v[\"summary\"])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3d4e4fe9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 메시지 핸들링을 위한 HumanMessage 클래스 임포트\n",
    "from langchain_core.messages import HumanMessage\n",
    "\n",
    "# 스레드 ID가 포함된 설정 객체 초기화\n",
    "config = {\"configurable\": {\"thread_id\": \"1\"}}\n",
    "\n",
    "# 첫 번째 사용자 메시지 생성 및 출력\n",
    "input_message = HumanMessage(content=\"안녕하세요? 반갑습니다. 제 이름은 테디입니다.\")\n",
    "input_message.pretty_print()\n",
    "\n",
    "# 스트림 모드에서 첫 번째 메시지 처리 및 업데이트 출력\n",
    "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"updates\"):\n",
    "    print_update(event)\n",
    "\n",
    "# 두 번째 사용자 메시지 생성 및 출력\n",
    "input_message = HumanMessage(content=\"제 이름이 뭔지 기억하세요?\")\n",
    "input_message.pretty_print()\n",
    "\n",
    "# 스트림 모드에서 두 번째 메시지 처리 및 업데이트 출력\n",
    "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"updates\"):\n",
    "    print_update(event)\n",
    "\n",
    "# 세 번째 사용자 메시지 생성 및 출력\n",
    "input_message = HumanMessage(content=\"제 직업은 AI 연구원이에요\")\n",
    "input_message.pretty_print()\n",
    "\n",
    "# 스트림 모드에서 세 번째 메시지 처리 및 업데이트 출력\n",
    "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"updates\"):\n",
    "    print_update(event)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0915ccb3",
   "metadata": {},
   "source": [
    "지금까지는 요약이 전혀 이루어지지 않은 것을 확인할 수 있습니다 - 이는 목록에 메시지가 6개밖에 없기 때문입니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d5037061",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 상태 구성 값 검색\n",
    "values = app.get_state(config).values\n",
    "values"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5402154f",
   "metadata": {},
   "source": [
    "이제 다른 메시지를 보내보겠습니다"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4f1b71cc",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 사용자 입력 메시지 객체 생성\n",
    "input_message = HumanMessage(\n",
    "    content=\"최근 LLM 에 대해 좀 더 알아보고 있어요. LLM 에 대한 최근 논문을 읽고 있습니다.\"\n",
    ")\n",
    "\n",
    "# 메시지 내용 출력\n",
    "input_message.pretty_print()\n",
    "\n",
    "# 스트림 이벤트 실시간 처리 및 업데이트 출력\n",
    "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"updates\"):\n",
    "    print_update(event)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7c1c0e18",
   "metadata": {},
   "source": [
    "현재 상태를 확인하면 대화의 요약과 함께 마지막 두 개의 메시지를 볼 수 있습니다."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "07780d0f",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 상태 구성 값 검색\n",
    "values = app.get_state(config).values\n",
    "values"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "20a49e8a",
   "metadata": {},
   "outputs": [],
   "source": [
    "messages = values[\"messages\"]\n",
    "messages"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3f7ccb46",
   "metadata": {},
   "source": [
    "이제 대화를 재개할 수 있습니다.\n",
    "\n",
    "마지막 두 개의 메시지만 있더라도 이전 대화 내용에 대해 질문할 수 있습니다 (이전 내용이 요약되어 있기 때문입니다)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "71c9c9b7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 사용자 메시지 객체 생성\n",
    "input_message = HumanMessage(content=\"제 이름이 무엇인지 기억하세요?\")\n",
    "\n",
    "# 메시지 내용 출력\n",
    "input_message.pretty_print()\n",
    "\n",
    "# 스트림 이벤트 실시간 처리 및 업데이트\n",
    "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"updates\"):\n",
    "    print_update(event)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "524a4d6e",
   "metadata": {},
   "outputs": [],
   "source": [
    "# 사용자 메시지 객체 생성\n",
    "input_message = HumanMessage(content=\"제 직업도 혹시 기억하고 계세요?\")\n",
    "\n",
    "# 메시지 내용 출력\n",
    "input_message.pretty_print()\n",
    "\n",
    "# 스트림 이벤트 실시간 처리 및 업데이트 출력\n",
    "for event in app.stream({\"messages\": [input_message]}, config, stream_mode=\"updates\"):\n",
    "    print_update(event)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "langchain-kr-lwwSZlnu-py3.11",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.11.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
